package ml.practicum.feature;

import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Properties;
import java.util.Scanner;
import java.util.Set;

import ml.practicum.feature.Feature.FeatureDataType;
import ml.practicum.feature.Feature.FeatureRole;
import ml.practicum.table.HeadedTable;

/**
 * A factory class that creates Featurs of the type LVFeature<String>.
 * @author jfs230
 *
 */
public class FeatureFactory {
	
	/**
	 * Builds A Feature based on properties file containing information about the role
	 * of the feature, and the discretization settings, a set of possible values, 
	 * a maximum of steps when discritisizing and a label naming the feature.
	 * <p>
	 * 
	 * </p>
	 * @param label
	 * @param set
	 * @param maxBins
	 * @param properties
	 * @return LVFeature<String> the feature with settings for processing values of type String
	 * @see LVFeature
	 */
	public static LVFeature<String> create(String label, Set<String> set, int maxBins, Properties properties){
		LVFeature<String> result = new LVFeature<String>(label);
		
		// determine what the role of the feature should be and set it
		if (Arrays.asList(properties.getProperty("inputs").split(",")).contains(label)){
			result.setRole(FeatureRole.INPUT);
		}else
		if(Arrays.asList(properties.getProperty("outputs").split(",")).contains(label)){
			result.setRole(FeatureRole.OUTPUT);
		}else{ result.setRole(FeatureRole.IGNORE);}
		 
		//Based on the set of input determine whether this feature is a numerical feature
		Set<Number> numericSet = getNumericSet(set);
		
		/* if the feature a numerical value set the processing values for that
		 * else set processing values for the nominal values in the set
		 */
		if (numericSet != null){
			//set to numerical
			result.setInputType(FeatureDataType.NUMERICAL);
			
			/* Set discrete steps based on the setSize and the maxBins allows 
			 * - number of steps = int((1/(1+e(-setSize/(maxBins/2)))  -(1/2))*2 *maxBins +1);
			 *  meaning: 	if setSize is way bigger than maxBins steps = maxBins
			 * 		  		if setSize is way smaller than maxBins steps = setSize
			 * 				if setSize is in the same order as maxBins steps is slightly smaller than maxBins 
			 */
			result.setDiscreteSteps(new Double((1./(1.+ Math.exp(-(numericSet.size()/(0.5*maxBins)))) -0.5)*2.*maxBins+1.).intValue());
			
			// Set the normal expected range to the maximum and minimum in the set 
			result.setMin(min(numericSet).intValue());
			result.setMax(max(numericSet).intValue());
			
			// Set the processing to the value as indicated in the given properties
			result.setProcessTo(FeatureDataType.valueOf(properties.getProperty("defaultAction")));
		}else{
			// Set input and output format to discrete 
			result.setInputType(FeatureDataType.DISCRETE);
			result.setProcessTo(FeatureDataType.DISCRETE);
			
			// make a mapping from the set.
			result.setMapping(new ArrayList<String>(set));
		}
		return result;
	}
	
	/**
	 * Based on the set of input determine whether this feature is a numerical feature.
	 * <p> If the input is a set of numerical values the methods returns a 
	 * <code>Set</code> of numbers. Otherwise the method returns <code>null</code>
	 * 
	 * @param set	the input set with string values
	 * @return the set with parsed numbers
	 */
	private static Set<Number> getNumericSet(Set<String> set){
		Set<Number> result = new HashSet<Number>(); 
		try{
			for(String element:set){
				result.add(Integer.parseInt(element));
			}
		}catch(Exception e){
			return null;
		}
		return result;
	}
	
	/**
	 * Returns the minimal value in a Collection of Numbers
	 * @param set	the Collection of numbers
	 * @return the lowest value in the list 
	 */
	private static Number min(Collection<Number> set){
		Number result= null;
		for(Number item:set){
			if (result == null || result.doubleValue() > item.doubleValue()){
				result = item;
			}
		}
		return result;
	}
	
	/**
	 * Returns the maximum value in a Collection of Numbers
	 * @param set	the Collection of numbers
	 * @return the highest value in the input list 
	 */
	private static Number max(Collection<Number> set){
		Number result= null;
		for(Number item:set){
			if (result == null || result.doubleValue() < item.doubleValue()){
				result = item;
			}
		}
		return result;
	}
	
	
	/**
	 * Builds A Feature based on a crap CLI
	 * Not to be used anymore.
	 * @deprecated Not to be used anymore because it contains no instruction no restore or redo option
	 * And it does not have priority to be fixed or developed. Use <code> create(String,int,Set,Properties instead)</code> 
	 */
	public static LVFeature<String> create(String label,InputStream input, PrintStream output, HeadedTable<String,String> data){
		Scanner in = new Scanner(input);
		LVFeature<String> result;
		if(label !=null){ 
			output.printf("\nLabel: %s",label);
			result = new LVFeature<String>(label);
		}else{
			output.print("\nLabel: ");
			result = new LVFeature<String>(in.nextLine().trim());			
		}
		String next = null;
		output.print("\nRole: ");
		next = in.nextLine();
		if (next.equals("i")){
			result.setRole(FeatureRole.INPUT);
		}else
		if (next.equals("o")) {
			result.setRole(FeatureRole.OUTPUT);
		}else 
		if (next.equals("g")) {
				result.setRole(FeatureRole.IGNORE);
		}
		
		if (result.getRole() != FeatureRole.IGNORE){
			
			output.print("\nInput: ");
			next = in.nextLine();
			if (next.equals("n")){
				result.setInputType(FeatureDataType.NUMERICAL);
			}else
			if (next.equals("d")) {
				result.setInputType(FeatureDataType.DISCRETE);
			}
			
			if(result.getInputType() == FeatureDataType.NUMERICAL || result.getRole() == FeatureRole.OUTPUT){
				output.print("\nOutput: ");
				next = in.nextLine();
				if (next.equals("n")){
					result.setProcessTo(FeatureDataType.NUMERICAL);
				}else
				if (next.equals("d")) {
					result.setProcessTo(FeatureDataType.DISCRETE);
				}
			}else{
				result.setProcessTo(FeatureDataType.DISCRETE);
			}
			
			for (int i= 0; i<data.getColumnCount();i++){
				HashSet<String> values = new HashSet<String>(data.getColumn(i));
				if (data.getLabel(i).equals(label)){
					if (result.getInputType() == FeatureDataType.DISCRETE){	
						result.setMax(values.size());
						result.setMapping(new ArrayList<String>(values));
					}else{
						Double max = 0.;
						for(String v:values){
							if (max < Double.valueOf(v)) max = Double.valueOf(v);
						}
						result.setMax(max.intValue()+1);
					}
				}
			}
		}
		return result;
	}
}
